package info.niwota.fwrite.imglist;

import java.io.Closeable;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;

import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore.Images.ImageColumns;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;

public class Util {
	private static final String TAG = "Util";
	public static final int DIRECTION_LEFT = 0;
	public static final int DIRECTION_RIGHT = 1;
	public static final int DIRECTION_UP = 2;
	public static final int DIRECTION_DOWN = 3;

	private static OnClickListener sNullOnClickListener;

	private Util() {
	}

	// Rotates the bitmap by the specified degree.
	// If a new bitmap is created, the original bitmap is recycled.
	public static Bitmap rotate(Bitmap b, int degrees) {
		if (degrees != 0 && b != null) {
			Matrix m = new Matrix();
			m.setRotate(degrees, (float) b.getWidth() / 2, (float) b
					.getHeight() / 2);
			try {
				Bitmap b2 = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b
						.getHeight(), m, true);
				if (b != b2) {
					b.recycle();
					b = b2;
				}
			} catch (OutOfMemoryError ex) {
				// We have no memory to rotate. Return the original bitmap.
			}
		}
		return b;
	}

	/*
	 * Compute the sample size as a function of minSideLength and
	 * maxNumOfPixels. minSideLength is used to specify that minimal width or
	 * height of a bitmap. maxNumOfPixels is used to specify the maximal size in
	 * pixels that is tolerable in terms of memory usage.
	 * 
	 * The function returns a sample size based on the constraints. Both size
	 * and minSideLength can be passed in as IImage.UNCONSTRAINED, which
	 * indicates no care of the corresponding constraint. The functions prefers
	 * returning a sample size that generates a smaller bitmap, unless
	 * minSideLength = IImage.UNCONSTRAINED.
	 * 
	 * Also, the function rounds up the sample size to a power of 2 or multiple
	 * of 8 because BitmapFactory only honors sample size this way. For example,
	 * BitmapFactory downsamples an image by 2 even though the request is 3. So
	 * we round up the sample size to avoid OOM.
	 */
	public static int computeSampleSize(BitmapFactory.Options options,
			int minSideLength, int maxNumOfPixels) {
		int initialSize = computeInitialSampleSize(options, minSideLength,
				maxNumOfPixels);

		int roundedSize;
		if (initialSize <= 8) {
			roundedSize = 1;
			while (roundedSize < initialSize) {
				roundedSize <<= 1;
			}
		} else {
			roundedSize = (initialSize + 7) / 8 * 8;
		}

		return roundedSize;
	}

	private static int computeInitialSampleSize(BitmapFactory.Options options,
			int minSideLength, int maxNumOfPixels) {
		double w = options.outWidth;
		double h = options.outHeight;

		int lowerBound = (maxNumOfPixels == IImage.UNCONSTRAINED) ? 1
				: (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
		int upperBound = (minSideLength == IImage.UNCONSTRAINED) ? 128
				: (int) Math.min(Math.floor(w / minSideLength), Math.floor(h
						/ minSideLength));

		if (upperBound < lowerBound) {
			// return the larger one when there is no overlapping zone.
			return lowerBound;
		}

		if ((maxNumOfPixels == IImage.UNCONSTRAINED)
				&& (minSideLength == IImage.UNCONSTRAINED)) {
			return 1;
		} else if (minSideLength == IImage.UNCONSTRAINED) {
			return lowerBound;
		} else {
			return upperBound;
		}
	}

	// Whether we should recycle the input (unless the output is the input).
	public static final boolean RECYCLE_INPUT = true;
	public static final boolean NO_RECYCLE_INPUT = false;

	public static Bitmap transform(Matrix scaler, Bitmap source,
			int targetWidth, int targetHeight, boolean scaleUp, boolean recycle) {
		int deltaX = source.getWidth() - targetWidth;
		int deltaY = source.getHeight() - targetHeight;
		if (!scaleUp && (deltaX < 0 || deltaY < 0)) {
			/*
			 * In this case the bitmap is smaller, at least in one dimension,
			 * than the target. Transform it by placing as much of the image as
			 * possible into the target and leaving the top/bottom or left/right
			 * (or both) black.
			 */
			Bitmap b2 = Bitmap.createBitmap(targetWidth, targetHeight,
					Bitmap.Config.ARGB_8888);
			Canvas c = new Canvas(b2);

			int deltaXHalf = Math.max(0, deltaX / 2);
			int deltaYHalf = Math.max(0, deltaY / 2);
			Rect src = new Rect(deltaXHalf, deltaYHalf, deltaXHalf
					+ Math.min(targetWidth, source.getWidth()), deltaYHalf
					+ Math.min(targetHeight, source.getHeight()));
			int dstX = (targetWidth - src.width()) / 2;
			int dstY = (targetHeight - src.height()) / 2;
			Rect dst = new Rect(dstX, dstY, targetWidth - dstX, targetHeight
					- dstY);
			c.drawBitmap(source, src, dst, null);
			if (recycle) {
				source.recycle();
			}
			return b2;
		}
		float bitmapWidthF = source.getWidth();
		float bitmapHeightF = source.getHeight();

		float bitmapAspect = bitmapWidthF / bitmapHeightF;
		float viewAspect = (float) targetWidth / targetHeight;

		if (bitmapAspect > viewAspect) {
			float scale = targetHeight / bitmapHeightF;
			if (scale < .9F || scale > 1F) {
				scaler.setScale(scale, scale);
			} else {
				scaler = null;
			}
		} else {
			float scale = targetWidth / bitmapWidthF;
			if (scale < .9F || scale > 1F) {
				scaler.setScale(scale, scale);
			} else {
				scaler = null;
			}
		}

		Bitmap b1;
		if (scaler != null) {
			// this is used for minithumb and crop, so we want to filter here.
			b1 = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source
					.getHeight(), scaler, true);
		} else {
			b1 = source;
		}

		if (recycle && b1 != source) {
			source.recycle();
		}

		int dx1 = Math.max(0, b1.getWidth() - targetWidth);
		int dy1 = Math.max(0, b1.getHeight() - targetHeight);

		Bitmap b2 = Bitmap.createBitmap(b1, dx1 / 2, dy1 / 2, targetWidth,
				targetHeight);

		if (b2 != b1) {
			if (recycle || b1 != source) {
				b1.recycle();
			}
		}

		return b2;
	}

	public static <T> int indexOf(T[] array, T s) {
		for (int i = 0; i < array.length; i++) {
			if (array[i].equals(s)) {
				return i;
			}
		}
		return -1;
	}

	public static void closeSilently(Closeable c) {
		if (c == null) {
			return;
		}
		try {
			c.close();
		} catch (Throwable t) {
			// do nothing
		}
	}

	public static void closeSilently(ParcelFileDescriptor c) {
		if (c == null)
			return;
		try {
			c.close();
		} catch (Throwable t) {
			// do nothing
		}
	}

//	/**
//	 * Make a bitmap from a given Uri.
//	 * 
//	 * @param uri
//	 */
//	public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels,
//			Uri uri, ContentResolver cr, boolean useNative) {
//		ParcelFileDescriptor input = null;
//		try {
//			input = cr.openFileDescriptor(uri, "r");
//			BitmapFactory.Options options = null;
////			if (useNative) {
////				options = createNativeAllocOptions();
////			}
//			return makeBitmap(minSideLength, maxNumOfPixels, input, options);
//		} catch (IOException e) {
//			return null;
//		} finally {
//			closeSilently(input);
//		}
//	}

	public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels,
			InputStream is, boolean useNative) {
		BitmapFactory.Options options = null;
		try {
			if (is == null) {
				return missingBitmap();
				// return null;
			}
			if (options == null) {
				options = new BitmapFactory.Options();
			}
			// options.inJustDecodeBounds = true;
			// BitmapManager.instance().decodeInputStream(is, options);
			// if (options.mCancel || options.outWidth == -1
			// || options.outHeight == -1) {
			// return null;
			// }
			// options.inSampleSize = computeSampleSize(options, minSideLength,
			// maxNumOfPixels);

			options.inJustDecodeBounds = false;

			options.inDither = false;
			options.inPreferredConfig = Bitmap.Config.ARGB_8888;
			return BitmapManager.instance().decodeInputStream(is, options);
		} catch (OutOfMemoryError e) {
			e.printStackTrace();
			Log.e(TAG, "Got oom exception ", e);
			return null;
		} finally {
			closeSilently(is);
		}
	}

	// public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels,
	// ParcelFileDescriptor pfd, boolean useNative) {
	// BitmapFactory.Options options = null;
	// if (useNative) {
	// options = createNativeAllocOptions();
	// }
	// return makeBitmap(minSideLength, maxNumOfPixels, null, null, pfd,
	// options);
	// }

	public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels,
			ParcelFileDescriptor pfd, BitmapFactory.Options options) {
		try {
			// if (pfd == null) {
			// pfd = makeInputStream(uri, cr);
			// }
			if (pfd == null) {
				return missingBitmap();
				// return null;
			}
			if (options == null) {
				options = new BitmapFactory.Options();
			}

			FileDescriptor fd = pfd.getFileDescriptor();
			
			options.inJustDecodeBounds = true;
			BitmapManager.instance().decodeFileDescriptor(fd, options);
			if (options.mCancel || options.outWidth == -1
					|| options.outHeight == -1) {
				return null;
			}
			
			options.inSampleSize = computeSampleSize(options, minSideLength,
					maxNumOfPixels);
			
			options.inJustDecodeBounds = false;
			options.inDither = false;
			options.inPreferredConfig = Bitmap.Config.ARGB_8888;
			return BitmapManager.instance().decodeFileDescriptor(fd, options);
		} catch (OutOfMemoryError e) {
			e.printStackTrace();
			Log.e(TAG, "Got oom exception ", e);
			return null;
		} finally {
			closeSilently(pfd);
		}
	}

	private static Bitmap missingBitmap() {
		InputStream is = null;
		try {
			is = Util.class.getResourceAsStream("/info/niwota/ezb/missing.png");
			return BitmapFactory.decodeStream(is);
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			closeSilently(is);
		}
		return null;
	}

	// private static Bitmap makeBitmap(byte[] jpegData, int maxNumOfPixels) {
	// try {
	// BitmapFactory.Options options = new BitmapFactory.Options();
	// options.inJustDecodeBounds = true;
	// BitmapFactory
	// .decodeByteArray(jpegData, 0, jpegData.length, options);
	// if (options.mCancel || options.outWidth == -1
	// || options.outHeight == -1) {
	// return null;
	// }
	// options.inSampleSize = computeSampleSize(options,
	// IImage.UNCONSTRAINED, maxNumOfPixels);
	// options.inJustDecodeBounds = false;
	//
	// options.inDither = false;
	// options.inPreferredConfig = Bitmap.Config.ARGB_8888;
	// return BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length,
	// options);
	// } catch (OutOfMemoryError ex) {
	// Log.e(TAG, "Got oom exception ", ex);
	// return null;
	// }
	// }

	// private static ParcelFileDescriptor makeInputStream(Uri uri,
	// ContentResolver cr) {
	// try {
	// return cr.openFileDescriptor(uri, "r");
	// } catch (Exception e) {
	// //e.printStackTrace();
	// return null;
	// }
	// }

	public static synchronized OnClickListener getNullOnClickListener() {
		if (sNullOnClickListener == null) {
			sNullOnClickListener = new OnClickListener() {
				public void onClick(View v) {
				}
			};
		}
		return sNullOnClickListener;
	}

	public static void Assert(boolean cond) {
		if (!cond) {
			throw new AssertionError();
		}
	}

	public static boolean equals(String a, String b) {
		// return true if both string are null or the content equals
		return a == b || a.equals(b);
	}

	// Returns an intent which is used for "set as" menu items.
	public static Intent createSetAsIntent(IImage image) {
		Uri u = image.fullSizeImageUri();
		Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
		intent.setDataAndType(u, image.getMimeType());
		intent.putExtra("mimeType", image.getMimeType());
		return intent;
	}

	// Returns Options that set the puregeable flag for Bitmap decode.
//	public static BitmapFactory.Options createNativeAllocOptions() {
//		BitmapFactory.Options options = new BitmapFactory.Options();
//		// options.inNativeAlloc = true;
//		return options;
//	}

	// public static void showFatalErrorAndFinish(
	// final Activity activity, String title, String message) {
	// DialogInterface.OnClickListener buttonListener =
	// new DialogInterface.OnClickListener() {
	// public void onClick(DialogInterface dialog, int which) {
	// activity.finish();
	// }
	// };
	// new AlertDialog.Builder(activity)
	// .setCancelable(false)
	// .setIcon(android.R.drawable.ic_dialog_alert)
	// .setTitle(title)
	// .setMessage(message)
	// .setNeutralButton(R.string.details_ok, buttonListener)
	// .show();
	// }

	public static Animation slideOut(View view, int to) {
		view.setVisibility(View.INVISIBLE);
		Animation anim;
		switch (to) {
		case DIRECTION_LEFT:
			anim = new TranslateAnimation(0, -view.getWidth(), 0, 0);
			break;
		case DIRECTION_RIGHT:
			anim = new TranslateAnimation(0, view.getWidth(), 0, 0);
			break;
		case DIRECTION_UP:
			anim = new TranslateAnimation(0, 0, 0, -view.getHeight());
			break;
		case DIRECTION_DOWN:
			anim = new TranslateAnimation(0, 0, 0, view.getHeight());
			break;
		default:
			throw new IllegalArgumentException(Integer.toString(to));
		}
		anim.setDuration(500);
		view.startAnimation(anim);
		return anim;
	}

	public static Animation slideIn(View view, int from) {
		view.setVisibility(View.VISIBLE);
		Animation anim;
		switch (from) {
		case DIRECTION_LEFT:
			anim = new TranslateAnimation(-view.getWidth(), 0, 0, 0);
			break;
		case DIRECTION_RIGHT:
			anim = new TranslateAnimation(view.getWidth(), 0, 0, 0);
			break;
		case DIRECTION_UP:
			anim = new TranslateAnimation(0, 0, -view.getHeight(), 0);
			break;
		case DIRECTION_DOWN:
			anim = new TranslateAnimation(0, 0, view.getHeight(), 0);
			break;
		default:
			throw new IllegalArgumentException(Integer.toString(from));
		}
		anim.setDuration(500);
		view.startAnimation(anim);
		return anim;
	}

	private static class BackgroundJob extends
			MonitoredActivity.LifeCycleAdapter implements Runnable {

		private final MonitoredActivity mActivity;
		private final ProgressDialog mDialog;
		private final Runnable mJob;
		private final Handler mHandler;
		private final Runnable mCleanupRunner = new Runnable() {
			public void run() {
				mActivity.removeLifeCycleListener(BackgroundJob.this);
				if (mDialog.getWindow() != null)
					mDialog.dismiss();
			}
		};

		public BackgroundJob(MonitoredActivity activity, Runnable job,
				ProgressDialog dialog, Handler handler) {
			mActivity = activity;
			mDialog = dialog;
			mJob = job;
			mActivity.addLifeCycleListener(this);
			mHandler = handler;
		}

		public void run() {
			try {
				mJob.run();
			} finally {
				mHandler.post(mCleanupRunner);
			}
		}

		@Override
		public void onActivityDestroyed(MonitoredActivity activity) {
			// We get here only when the onDestroyed being called before
			// the mCleanupRunner. So, run it now and remove it from the queue
			mCleanupRunner.run();
			mHandler.removeCallbacks(mCleanupRunner);
		}

		@Override
		public void onActivityStopped(MonitoredActivity activity) {
			mDialog.hide();
		}

		@Override
		public void onActivityStarted(MonitoredActivity activity) {
			mDialog.show();
		}
	}

	public static void startBackgroundJob(MonitoredActivity activity,
			String title, String message, Runnable job, Handler handler) {
		// Make the progress dialog uncancelable, so that we can gurantee
		// the thread will be done before the activity getting destroyed.
		ProgressDialog dialog = ProgressDialog.show(activity, title, message,
				true, false);
		new Thread(new BackgroundJob(activity, job, dialog, handler)).start();
	}

	public static String getDataPath(Context ctx, Uri uri) {
		if (uri != null) {
			Cursor c = ctx.getContentResolver().query(uri,
					new String[] { ImageColumns.DATA }, null, null, null);
			c.moveToFirst();
			String p = c.getString(0);
			c.close();
			// if (Debug.DEBUG) {
			// Log.d(TAG, "getDataPath " + p);
			// }
			return p;
		}
		return null;
	}
}
